package pl.enigmatic.parser.xml;

import static pl.enigmatic.parser.xml.MapPropertyExtension.modify;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Map.Entry;

import org.w3c.dom.Node;

import pl.enigmatic.math.AliasedMap;
import pl.enigmatic.reflection.Methods;
import pl.enigmatic.util.Reflection;
import pl.enigmatic.util.Strings;
import pl.enigmatic.parser.XMLAccessible;
import pl.enigmatic.parser.XMLAlias;
import pl.enigmatic.parser.XMLConverter;
import pl.enigmatic.parser.metadata.Metadata;
import pl.enigmatic.parser.string.Converter;


public class CollectionPropertyExtension implements XMLParserExtension {

	public static CollectionPropertyExtension createCustom(final Parser<?> owner, final String property, final String setter, final Class<?> type) {
		return new CollectionPropertyExtension(owner, property, setter, type);
	}

	private static class Setter implements PropertySetter {

		private final Field field;
		private final Class<?> type;
		private final Parser<?> parser;
		private final Method setter;
		private final String item;

		public Setter(final Field f, final Converter<?> c, final Method setter, final Class<?> type) {
			field = f;
			this.type = type;
			parser = type != null ? Parser.createParser(type).copy(c) : null;
			this.setter = setter;
			field.setAccessible(true);
			if (f.isAnnotationPresent(XMLAccessible.Item.class)) {
				item = f.getAnnotation(XMLAccessible.Item.class).value();
			} else {
				item = null;
			}
		}

		Setter(final Field f, final Converter<?> c, final Method setter) {
			this(f, c, setter, f.getAnnotation(XMLAccessible.class).value());
		}

		private boolean access(final Object target, final Object value) {
			try {
				if (setter != null) {
					setter.invoke(target, value);
				} else {
					Methods.add.invoke(field.get(target), value);
				}
				return true;
			} catch (final Exception e) {}
			return false;
		}

		@Override
		public boolean access(Object target, Object value, ParsingInfo<?> info) {
			return access(target, value);
		}

		@Override
		public String getPropertyName() {
			return field.getName();
		}

		@Override
		public Parser<?> getParser() {
			return parser;
		}
	}

	/** public methods for setting property values */
	private final AliasedMap<String, Setter> setters = new AliasedMap<String, Setter>();
	private final Parser<?> owner;

	CollectionPropertyExtension(final Parser<?> owner) {
		this.owner = owner;
		final Class<?> clazz = owner.getClazz();
		for (final Entry<String, Field> e : Reflection.getDeclaredFields(clazz).entrySet()) {
			final String key = e.getKey();
			final Field f = e.getValue();
			if (Reflection.isCollection(f.getType()) && f.isAnnotationPresent(XMLAccessible.class)) {
				Converter<?> converter = null;
				if (f.isAnnotationPresent(XMLConverter.class)) {
					converter = Converter.create(f.getAnnotation(XMLConverter.class).value());
				}
				Method setter = null;
				for (final Method m : clazz.getDeclaredMethods()) {
					if (m.isAnnotationPresent(XMLAccessible.Setter.class) && m.getAnnotation(XMLAccessible.Setter.class).value().equals(f.getName())) {
						setter = m;
						if (m.isAnnotationPresent(XMLConverter.class)) {
							converter = Converter.create(m.getAnnotation(XMLConverter.class).value());
						}
						break;
					}
				}
				setters.put(key, new Setter(f, converter, setter));
				if (setter != null && setter.isAnnotationPresent(XMLAlias.class)) {
					setters.addAliases(key, setter.getAnnotation(XMLAlias.class).value());
				}
				if (f != null && f.isAnnotationPresent(XMLAlias.class)) {
					setters.addAliases(key, f.getAnnotation(XMLAlias.class).value());
				}
			}
		}
	}

	public CollectionPropertyExtension(final Parser<?> owner, final String property, final String setter, final Class<?> type) {
		this.owner = owner;
		final Class<?> clazz = owner.getClazz();
		Field f = null;
		try {
			f = Reflection.getDeclaredFieldRecursively(clazz, property);
		} catch (final Exception e) {}
		Converter<?> converter = null;
		if (f != null && f.isAnnotationPresent(XMLConverter.class)) {
			converter = Converter.create(f.getAnnotation(XMLConverter.class).value());
		}
		Method method = null;
		try {
			method = clazz.getMethod(setter, type);
		} catch (final Exception e) {}
		if (method != null && method.isAnnotationPresent(XMLConverter.class)) {
			converter = Converter.create(method.getAnnotation(XMLConverter.class).value());
		}
		setters.put(property, new Setter(f, converter, method, type));
	}

	@SuppressWarnings("unchecked")
	@Override
	public NodeNameInfo parseNodeName(final Node node, final ParsingInfo<?> info) {
		if (owner.hasContent() && setters.containsKey(owner.getContent())
				&& (Metadata.ITEM.equals(node) || node.getNodeName().equals(setters.get(owner.getContent()).item))) {
			final Setter s = setters.get(owner.getContent());
			return new NodeNameInfo(owner.getContent(), s.type, s.parser);
		} else {
			final String property = owner.getPropertyName(node.getLocalName());
			if (node.getPrefix() == null && !property.contains(".") && Strings.lowerStarted(property)) {
				if (setters.containsKey(property)) {
					final Setter s = setters.get(property);
					modify(node, s.item);
					return new NodeNameInfo(property, s.type, CollectionParser.createCollectionParser(s.type).copy(s.parser));
				}
				return null;
			}
		}
		return null;
	}

	@Override
	public boolean setParsedProperty(final Object target, final ParsingInfo<?> info, final Object value) {
		final Setter s = setters.get(info.getNodeInfo().getProperty());
		final Collection<?> collection = (Collection<?>) value;
		if (s.setter != null) {
			for (final Object e : collection) {
				if (!s.access(target, e)) { return false; }
			}
		} else {
			try {
				Methods.addAll.invoke(s.field.get(target), value);
			} catch (final Exception e) {
				return false;
			}
		}
		return true;
	}

	@Override
	public PropertySetter accessor(final String property) {
		return setters.get(property);
	}
}
